// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Bridge to aid in communication between a Chrome
 * background page and content script.
 *
 * It automatically figures out where it's being run and initializes itself
 * appropriately. Then just call send() to send a message from the background
 * to the page or vice versa, and addMessageListener() to provide a message
 * listener.  Messages can be any object that can be serialized using JSON.
 *
 */

goog.provide('ExtensionBridge');

ExtensionBridge = class {
  constructor() {}

  /**
   * Initialize the extension bridge. Dynamically figure out whether we're in
   * the background page, content script, or in a page, and call the
   * corresponding function for more specific initialization.
   */
  static init() {
    const self = ExtensionBridge;
    self.messageListeners = [];
    self.disconnectListeners = [];

    if (/^chrome-extension:\/\/.*background\.html$/.test(
            window.location.href)) {
      // This depends on the fact that the background page has a specific url.
      // We should never be loaded into another extension's background page, so
      // this is a safe check.
      self.context = self.BACKGROUND;
      self.initBackground();
      return;
    }

    if (chrome && chrome.extension) {
      self.context = self.CONTENT_SCRIPT;
      self.initContentScript();
    }
  }

  /**
   * Send a message. If the context is a page, sends a message to the
   * extension background page. If the context is a background page, sends
   * a message to the current active tab (not all tabs).
   *
   * @param {Object} message The message to be sent.
   */
  static send(message) {
    const self = ExtensionBridge;
    switch (self.context) {
      case self.BACKGROUND:
        self.sendBackgroundToContentScript(message);
        break;
      case self.CONTENT_SCRIPT:
        self.sendContentScriptToBackground(message);
        break;
    }
  }

  /**
   * Provide a function to listen to messages. In page context, this
   * listens to messages from the background. In background context,
   * this listens to messages from all pages.
   *
   * The function gets called with two parameters: the message, and a
   * port that can be used to send replies.
   *
   * @param {function(Object, Port)} listener The message listener.
   */
  static addMessageListener(listener) {
    ExtensionBridge.messageListeners.push(listener);
  }

  /**
   * Provide a function to be called when the connection is
   * disconnected.
   *
   * @param {function()} listener The listener.
   */
  static addDisconnectListener(listener) {
    ExtensionBridge.disconnectListeners.push(listener);
  }

  /**
   * Removes all message listeners from the extension bridge.
   */
  static removeMessageListeners() {
    ExtensionBridge.messageListeners.length = 0;
  }

  /**
   * Returns a unique id for this instance of the script.
   *
   * @return {number}
   */
  static uniqueId() {
    return ExtensionBridge.id_;
  }

  /**
   * Initialize the extension bridge in a background page context by registering
   * a listener for connections from the content script.
   */
  static initBackground() {
    const self = ExtensionBridge;

    /** @type {!Array<Port>} @private */
    self.portCache_ = [];
    /** @type {number} */
    self.nextPongId_ = 1;
    /** @type {number} */
    self.id_ = 0;

    const onConnectHandler = function(port) {
      if (port.name !== self.PORT_NAME) {
        return;
      }

      self.portCache_.push(port);

      port.onMessage.addListener(function(message) {
        if (message[ExtensionBridge.PING_MSG]) {
          const pongMessage = {};
          pongMessage[ExtensionBridge.PONG_MSG] = self.nextPongId_++;
          port.postMessage(pongMessage);
          return;
        }

        for (let i = 0; i < self.messageListeners.length; i++) {
          self.messageListeners[i](message, port);
        }
      });

      port.onDisconnect.addListener(function(message) {
        for (let i = 0; i < self.portCache_.length; i++) {
          if (self.portCache_[i] === port) {
            self.portCache_.splice(i, 1);
            break;
          }
        }
      });
    };

    chrome.extension.onConnect.addListener(onConnectHandler);
  }

  /**
   * Initialize the extension bridge in a content script context, listening
   * for messages from the background page.
   */
  static initContentScript() {
    const self = ExtensionBridge;
    self.connected = false;
    self.pingAttempts = 0;
    self.queuedMessages = [];
    /** @type {number} */
    self.id_ = -1;

    const onMessageHandler = function(request, sender, sendResponse) {
      if (request && request['srcFile']) {
        // TODO (clchen, deboer): Investigate this further and come up with a
        // cleaner solution. The root issue is that this should never be run on
        // the background page, but it is in the Chrome OS case.
        return;
      }
      if (request[ExtensionBridge.PONG_MSG]) {
        self.gotPongFromBackgroundPage(request[ExtensionBridge.PONG_MSG]);
      } else {
        for (let i = 0; i < self.messageListeners.length; i++) {
          self.messageListeners[i](request, ExtensionBridge.backgroundPort);
        }
      }
      sendResponse({});
    };

    // Listen to requests from the background that don't come from
    // our connection port.
    chrome.extension.onMessage.addListener(onMessageHandler);

    self.setupBackgroundPort();

    self.tryToPingBackgroundPage();
  }

  /**
   * Set up the connection to the background page.
   */
  static setupBackgroundPort() {
    // Set up the connection to the background page.
    const self = ExtensionBridge;
    self.backgroundPort = chrome.extension.connect({name: self.PORT_NAME});
    if (!self.backgroundPort) {
      return;
    }
    self.backgroundPort.onMessage.addListener(function(message) {
      if (message[ExtensionBridge.PONG_MSG]) {
        self.gotPongFromBackgroundPage(message[ExtensionBridge.PONG_MSG]);
      } else {
        for (let i = 0; i < self.messageListeners.length; i++) {
          self.messageListeners[i](message, self.backgroundPort);
        }
      }
    });
    self.backgroundPort.onDisconnect.addListener(function(event) {
      // If we're not connected yet, don't give up - try again.
      if (!self.connected) {
        self.backgroundPort = null;
        return;
      }

      self.backgroundPort = null;
      self.connected = false;
      for (let i = 0; i < self.disconnectListeners.length; i++) {
        self.disconnectListeners[i]();
      }
    });
  }

  /**
   * Try to ping the background page.
   */
  static tryToPingBackgroundPage() {
    const self = ExtensionBridge;

    // If we already got a pong, great - we're done.
    if (self.connected) {
      return;
    }

    self.pingAttempts++;
    if (self.pingAttempts > 5) {
      // Could not connect after 5 ping attempts. Call the disconnect
      // handlers, which will disable ChromeVox.
      for (let i = 0; i < self.disconnectListeners.length; i++) {
        self.disconnectListeners[i]();
      }
      return;
    }

    // Send the ping.
    const msg = {};
    msg[ExtensionBridge.PING_MSG] = 1;
    if (!self.backgroundPort) {
      self.setupBackgroundPort();
    }
    if (self.backgroundPort) {
      self.backgroundPort.postMessage(msg);
    }

    // Check again in 500 ms in case we get no response.
    window.setTimeout(ExtensionBridge.tryToPingBackgroundPage, 500);
  }

  /**
   * Got pong from the background page, now we know the connection was
   * successful.
   * @param {number} pongId unique id assigned to us by the background page
   */
  static gotPongFromBackgroundPage(pongId) {
    const self = ExtensionBridge;
    self.connected = true;
    self.id_ = pongId;

    while (self.queuedMessages.length > 0) {
      self.sendContentScriptToBackground(self.queuedMessages.shift());
    }
  }

  /**
   * Send a message from the content script to the background page.
   *
   * @param {Object} message The message to send.
   */
  static sendContentScriptToBackground(message) {
    const self = ExtensionBridge;
    if (!self.connected) {
      // We're not connected to the background page, so queue this message
      // until we're connected.
      self.queuedMessages.push(message);
      return;
    }

    if (ExtensionBridge.backgroundPort) {
      ExtensionBridge.backgroundPort.postMessage(message);
    } else {
      chrome.extension.sendMessage(message);
    }
  }

  /**
   * Send a message from the background page to the content script of the
   * current selected tab.
   *
   * @param {Object} message The message to send.
   */
  static sendBackgroundToContentScript(message) {
    ExtensionBridge.portCache_.forEach(function(port) {
      port.postMessage(message);
    });
  }
};


/**
 * Constant indicating we're in a background page.
 * @type {number}
 * @const
 */
ExtensionBridge.BACKGROUND = 0;

/**
 * Constant indicating we're in a content script.
 * @type {number}
 * @const
 */
ExtensionBridge.CONTENT_SCRIPT = 1;

/**
 * The name of the port between the content script and background page.
 * @type {string}
 * @const
 */
ExtensionBridge.PORT_NAME = 'ExtensionBridge.Port';

/**
 * The name of the message between the content script and background to
 * see if they're connected.
 * @type {string}
 * @const
 */
ExtensionBridge.PING_MSG = 'ExtensionBridge.Ping';

/**
 * The name of the message between the background and content script to
 * confirm that they're connected.
 * @type {string}
 * @const
 */
ExtensionBridge.PONG_MSG = 'ExtensionBridge.Pong';


ExtensionBridge.init();
